local Class = require('lua.lang.Class')
local DataInputStream = require('lua.io.DataInputStream')
local LuaString = require('lua.vm.LuaString')
local LuaNumber = require('lua.vm.LuaNumber')
local Double = require('lua.lang.Double')
local LuaNil = require('lua.vm.LuaNil')
local LuaBoolean = require('lua.vm.LuaBoolean')
local UpvalueDescriptor = require('lua.vm.UpvalueDescriptor')
local LocalVariable = require('lua.vm.LocalVariable')
local Prototype = require('lua.vm.Prototype')

--- @class LoadState : Class
--- @field luacVersion number
--- @field luacFormat number
--- @field luacLittleEndian boolean
--- @field luacSizeofInt number
--- @field luacSizeofSizeT number
--- @field luacSizeofInstruction number
--- @field luacSizeofLuaNumber number
--- @field luacNumberFormat number
--- @field is DataInputStream
--- @field name string
--- @field buf number[]
local LoadState = Class {}

LoadState.badHeader = false
LoadState.NUMBER_FORMAT_FLOATS_OR_DOUBLES = 0
LoadState.NUMBER_FORMAT_INTS_ONLY = 1
LoadState.NUMBER_FORMAT_NUM_PATCH_INT32 = 4

LoadState.LUA_TINT = -2
LoadState.LUA_TNONE = -1
LoadState.LUA_TNIL = 0
LoadState.LUA_TBOOLEAN = 1
LoadState.LUA_TLIGHTUSERDATA = 2
LoadState.LUA_TNUMBER = 3
LoadState.LUA_TSTRING = 4
LoadState.LUA_TTABLE = 5
LoadState.LUA_TFUNCTION = 6
LoadState.LUA_TUSERDATA = 7
LoadState.LUA_TTHREAD = 8
LoadState.LUA_TVALUE = 9
--- @type number[]
LoadState.LUA_SIGNATURE = { 0x1B, 0x4C, 0x75, 0x61 }
--- @type number[]
LoadState.LUAC_TAIL = { 0x19, 0x93, 0x0D, 0x0A, 0x1A, 0x0A }
LoadState.SOURCE_BINARY_STRING = '=?'
LoadState.LUAC_VERSION = 0x52
LoadState.LUAC_FORMAT = 0
LoadState.LUAC_HEADERSIZE = 12

--- @param stream InputStream
--- @param name string
--- @return LoadState
function LoadState:new(stream, name)
	--- @type LoadState
	local o = {}
	o.is = DataInputStream:new(stream)
	o.name = name
	o.luacVersion = 0
	o.luacFormat = 0
	o.luacLittleEndian = false
	o.luacSizeofInt = 0
	o.luacSizeofSizeT = 0
	o.luacSizeofInstruction = 0
	o.luacSizeofLuaNumber = 0
	o.luacNumberFormat = 0
	o.buf = {}
	return setmetatable(o, LoadState)
end

--- @return number
function LoadState:loadInt()
	local buf = self.buf
	self.is:readFully(buf, 1, 4)
	return self.luacLittleEndian and
			(buf[4] << 24) | ((0xff & buf[3]) << 16) | ((0xff & buf[2]) << 8) | (0xff & buf[1]) or
			(buf[1] << 24) | ((0xff & buf[2]) << 16) | ((0xff & buf[3]) << 8) | (0xff & buf[4])
end

--- @return number[]
function LoadState:loadIntArray()
	local n = self:loadInt()
	if n == 0 then
		return {}
	end
	local buf = self.buf
	self.is:readFully(buf, 1, n << 2)
	local array = {}
	local j = 1
	for i = 1, n do
		array[i] = self.luacLittleEndian and
				(buf[j + 3] << 24) | ((0xff & buf[j + 2]) << 16) | ((0xff & buf[j + 1]) << 8) | (0xff & buf[j]) or
				(buf[j] << 24) | ((0xff & buf[j + 1]) << 16) | ((0xff & buf[j + 2]) << 8) | (0xff & buf[j + 3])
		j = j + 4
	end
	return array
end

--- @return number
function LoadState:loadInt64()
	--- @type number
	local a, b
	if self.luacLittleEndian then
		a = self:loadInt()
		b = self:loadInt()
	else
		b = self:loadInt()
		a = self:loadInt()
	end
	return (b << 32) | (a & 0xffffffff)
end

--- @return LuaString
function LoadState:loadString()
	local size = self.luacSizeofSizeT == 8 and self:loadInt64() or self:loadInt()
	if size == 0 then
		return nil
	end
	--- @type number[]
	local bytes = {}
	self.is:readFully(bytes, 1, size)
	return LuaString:valueUsing(bytes, 1, size - 1)
end

--- @param bits number
--- @return LuaNumber
function LoadState:longBitsToLuaNumber(bits)
	if bits & ((1 << 63) - 1) == 0 then
		return LuaNumber.ZERO
	end
	local e = ((bits >> 52) & 0x7ff) - 1023
	if e >= 0 and e < 31 then
		local f = bits & 0xFFFFFFFFFFFFF
		local shift = 52 - e
		local intPrecMask = (1 << shift) - 1
		if f & intPrecMask == 0 then
			local intValue = (f >> shift) | (1 << e)
			return LuaNumber:valueOf((bits >> 63) ~= 0 and -intValue or intValue)
		end
	end
	return LuaNumber:valueOf(Double:longBitsToDouble(bits))
end

--- @return LuaNumber
function LoadState:loadNumber()
	if self.luacNumberFormat == self.NUMBER_FORMAT_INTS_ONLY then
		return LuaNumber:valueOf(self:loadInt())
	end
	return self:longBitsToLuaNumber(self:loadInt64())
end

--- @param f Prototype
--- @return void
function LoadState:loadConstants(f)
	local n = self:loadInt()
	--- @type LuaValue[]
	local values = {}
	local is = self.is
	for i = 1, n do
		local t = is:readByte()
		if t == self.LUA_TNIL then
			values[i] = LuaNil._NIL
		elseif t == self.LUA_TBOOLEAN then
			values[i] = is:readByte() ~= 0 and LuaBoolean._TRUE or LuaBoolean._FALSE
		elseif t == self.LUA_TINT then
			values[i] = LuaNumber:valueOf(self:loadInt())
		elseif t == self.LUA_TNUMBER then
			values[i] = self:loadNumber()
		elseif t == self.LUA_TSTRING then
			values[i] = self:loadString()
		else
			error('bad constant')
		end
	end
	f.k = values
	n = self:loadInt()
	--- @type Prototype[]
	local p = {}
	for i = 1, n do
		p[i] = self:loadFunction()
	end
	f.p = p
end

--- @param f Prototype
--- @return void
function LoadState:loadUpvalues(f)
	local n = self:loadInt()
	--- @type UpvalueDescriptor[]
	local upvalues = {}
	local is = self.is
	for i = 1, n do
		upvalues[i] = UpvalueDescriptor:new(
				nil,
				is:readByte() ~= 0,
				is:readByte() & 0xff
		)
	end
	f.upvalues = upvalues
end

--- @param f Prototype
--- @return void
function LoadState:loadDebug(f)
	f.source = self:loadString()
	f.lineInfo = self:loadIntArray()
	local n = self:loadInt()
	--- @type LocalVariable[]
	local locvars = {}
	for i = 1, n do
		local varName = self:loadString()
		local startPc = self:loadInt()
		local endPc = self:loadInt()
		locvars[i] = LocalVariable:new(varName, startPc, endPc)
	end
	f.locVars = locvars
	n = self:loadInt()
	local upvalues = f.upvalues
	for i = 1, n do
		upvalues[i].variableName = self:loadString()
	end
end

--- @return Prototype
function LoadState:loadFunction()
	local f = Prototype:new()
	local is = self.is
	f.lineDefined = self:loadInt()
	f.lastLineDefined = self:loadInt()
	f.numParams = is:readByte()
	f.isVararg = is:readByte()
	f.maxStackSize = is:readByte()
	f.code = self:loadIntArray()
	self:loadConstants(f)
	self:loadUpvalues(f)
	self:loadDebug(f)
	return f
end

--- @return void
function LoadState:loadHeader()
	local is = self.is
	self.luacVersion = is:readByte()
	self.luacFormat = is:readByte()
	self.luacLittleEndian = 0 ~= is:readByte()
	self.luacSizeofInt = is:readByte()
	self.luacSizeofSizeT = is:readByte()
	self.luacSizeofInstruction = is:readByte()
	self.luacSizeofLuaNumber = is:readByte()
	self.luacNumberFormat = is:readByte()
	local tail = self.LUAC_TAIL
	for i = 1, #tail do
		if is:readByte() ~= tail[i] then
			error('Unexpeted byte in luac tail of header, index=' .. tostring(i))
		end
	end
end

--- @param stream InputStream
--- @param chunkName string
--- @param checkHeader boolean
--- @return Prototype
function LoadState:unDump(stream, chunkName, checkHeader)
	local srcName = chunkName == nil and nil or self:getSourceName(chunkName)
	local state = LoadState:new(stream, srcName)
	if checkHeader then
		if not state:checkHeader() then
			return nil
		end
	end
	state:loadHeader()
	if chunkName == nil then
		return nil
	end
	local luacNumberFormat = state.luacNumberFormat
	if luacNumberFormat == self.NUMBER_FORMAT_FLOATS_OR_DOUBLES or
			luacNumberFormat == self.NUMBER_FORMAT_INTS_ONLY or
			luacNumberFormat == self.NUMBER_FORMAT_NUM_PATCH_INT32 then
		return self:fillSource(state:loadFunction(), LuaString:valueOf(srcName))
	else
		error('unsupported int size')
	end
end

--- @param p Prototype
--- @param def LuaString
--- @return Prototype
function LoadState:fillSource(p, def)
	if p.source == nil then
		p.source = def
	end
	local sp = p.p
	local source = p.source
	for i = 1, #sp do
		self:fillSource(sp[i], source)
	end
	return p
end

--- @param name string
--- @return string
function LoadState:getSourceName(name)
	local srcName = name
	if name:startsWith('@') or name:startsWith('=') then
		srcName = name:sub(2)
	elseif name:startsWith('\033') then
		srcName = self.SOURCE_BINARY_STRING
	end
	return srcName
end

--- @return boolean
function LoadState:checkHeader()
	local sign = self.LUA_SIGNATURE
	local is = self.is
	local res = true
	for i = 1, #sign do
		if is:readByte() ~= sign[i] then
			res = false
		end
	end
	return res
end

return LoadState